Skip to content

feat: env: map in mantle.yaml for workflow expression variables (#74)#119

Merged
michaelmcnees merged 7 commits into
mainfrom
feature/env-map-config
Mar 27, 2026
Merged

feat: env: map in mantle.yaml for workflow expression variables (#74)#119
michaelmcnees merged 7 commits into
mainfrom
feature/env-map-config

Conversation

@michaelmcnees

@michaelmcnees michaelmcnees commented Mar 27, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds an optional env: section to mantle.yaml that populates the env.* namespace in CEL expressions, merged with MANTLE_ENV_* shell environment variables at runtime.

  • env: values in YAML serve as defaults
  • MANTLE_ENV_* environment variables take precedence (override YAML values)
  • An slog.Info message is emitted when an env var overrides a config value
  • Viper lowercases YAML map keys, so a post-unmarshal normalization step uppercases them to match MANTLE_ENV_* convention

Closes #74

Example

version: 1
env:
  SLACK_CHANNEL: "#ops-alerts"
  ENVIRONMENT: production
steps:
  - name: notify
    action: slack/send
    params:
      channel: "{{ env.SLACK_CHANNEL }}"

Test plan

  • Config parsing: env: section parses into map[string]string
  • Empty/missing env: section results in nil map (no error)
  • Config env values accessible in CEL as env.KEY
  • MANTLE_ENV_* overrides config value
  • SetConfigEnv(nil) does not panic
  • Full test suite passes (23 packages)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Workflow expressions can read a configurable env map (config values merged with MANTLE_ENV_* OS vars; OS values take precedence) and the CLI initializes this env at startup.
    • Runtime API to update the configured env so subsequent evaluations use the merged values; overrides emit an informational log.
  • Documentation

    • Added env: example, usage, and precedence/override notes.
  • Tests

    • New tests for merge behavior, overrides, and nil-config handling.

michaelmcnees and others added 4 commits March 27, 2026 00:21
Add Env map[string]string field to Config struct, parsed from the env:
section in mantle.yaml. Keys are normalized to uppercase to match the
MANTLE_ENV_* convention used by the CEL evaluator.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rename envVars() to mergeEnvVars(configEnv) which starts with config
values from mantle.yaml env: section, then overlays MANTLE_ENV_*
OS environment variables. When a key exists in both sources, the OS
env var wins and an info log is emitted. Add SetConfigEnv setter to
avoid changing the NewEvaluator signature.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Call eng.CEL.SetConfigEnv(cfg.Env) after engine.New(database) in the
run and serve CLI commands so that env: values from mantle.yaml are
available in CEL workflow expressions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add Workflow Expression Variables section to configuration docs covering
the env: YAML syntax, CEL expression usage, and MANTLE_ENV_* override
precedence with info logging on conflicts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Mar 27, 2026

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@michaelmcnees has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 4 minutes and 31 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 4 minutes and 31 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1990d674-08ec-4cb2-988f-b652c7902ea8

📥 Commits

Reviewing files that changed from the base of the PR and between cdaab76 and 3fbecb7.

📒 Files selected for processing (1)
  • packages/site/src/content/docs/configuration.md
📝 Walkthrough

Walkthrough

Adds an optional env: map to the application config and wires it into the CEL evaluator via Evaluator.SetConfigEnv(). The evaluator now merges config-provided env keys with OS MANTLE_ENV_* variables (OS wins) and logs overrides; CLI initializes the evaluator with cfg.Env.

Changes

Cohort / File(s) Summary
CEL environment logic
packages/engine/internal/cel/cel.go
Replaced envVars() with mergeEnvVars(configEnv), added configEnv field to Evaluator, added exported SetConfigEnv(configEnv map[string]string), rebuilt envCache on set, and emit slog.Info when an OS MANTLE_ENV_* overrides a config value.
CEL tests
packages/engine/internal/cel/cel_test.go
Updated tests to use mergeEnvVars(nil), added tests for config merge, OS override precedence, and nil config handling.
CLI initialization
packages/engine/internal/cli/run.go, packages/engine/internal/cli/serve.go
After engine.New(database), call eng.CEL.SetConfigEnv(cfg.Env) to populate evaluator env from the loaded config.
Config structure & tests
packages/engine/internal/config/config.go, packages/engine/internal/config/config_test.go
Added exported Env map[string]string \mapstructure:"env"`toConfig, normalize keys to uppercase in Load(), and added tests TestLoad_EnvMapandTestLoad_EnvMapEmpty`.
Documentation
packages/site/src/content/docs/configuration.md
Documented env: config section and example, added env.* entry in config table, explained CEL env.<KEY> usage and merge precedence (OS overrides config), and updated a default value.

Sequence Diagram(s)

sequenceDiagram
  participant CLI as "CLI (run/serve)"
  participant Config as "Config Loader\n(mantle.yaml)"
  participant Engine as "Engine"
  participant Evaluator as "CEL Evaluator"
  participant OS as "OS Env (MANTLE_ENV_*)"
  participant Workflow as "Workflow Runner"

  CLI->>Config: load config (includes env map)
  CLI->>Engine: engine.New(database)
  Engine->>Evaluator: eng.CEL.SetConfigEnv(cfg.Env)
  Evaluator->>OS: read MANTLE_ENV_* variables
  Evaluator->>Evaluator: mergeEnvVars(configEnv, OS)  -- OS wins on conflicts
  CLI->>Workflow: execute workflow (uses env.<KEY> in CEL)
  Workflow->>Evaluator: evaluate expressions referencing env.<KEY>
  Evaluator-->>Workflow: evaluated values
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐇 I found a YAML patch and gave a cheer,
I mixed file carrots with the OS thyme here.
Env keys now mingle, but the shell leads the tune,
I hopped, logged the override, and hummed to the moon.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.08% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately summarizes the main change: adding an env: map to mantle.yaml for workflow expression variables, which aligns with the primary objective.
Linked Issues check ✅ Passed All coding requirements from issue #74 are met: env: map added to config, merged with MANTLE_ENV_* variables with proper precedence, logging on overrides, and comprehensive test coverage.
Out of Scope Changes check ✅ Passed All changes are directly related to issue #74: adding env: map support to mantle.yaml, integrating it into CEL evaluation, and updating documentation. No unrelated modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/env-map-config

Comment @coderabbitai help to get the list of available commands and usage tips.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Mar 27, 2026

Copy link
Copy Markdown

Deploying mantle with  Cloudflare Pages  Cloudflare Pages

Latest commit: 3fbecb7
Status: ✅  Deploy successful!
Preview URL: https://053723c4.mantle-cxy.pages.dev
Branch Preview URL: https://feature-env-map-config.mantle-cxy.pages.dev

View logs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
packages/engine/internal/config/config_test.go (1)

313-333: Consider adding a test case for case normalization.

The test uses uppercase keys in the YAML (APP_NAME, REGION, DEBUG), which matches the expected output. However, since config.go normalizes keys to uppercase via strings.ToUpper, consider adding a test case with lowercase/mixed-case YAML keys to verify the normalization logic works correctly.

📋 Example additional test case
+func TestLoad_EnvMapCaseNormalization(t *testing.T) {
+	dir := t.TempDir()
+	configFile := filepath.Join(dir, "mantle.yaml")
+	err := os.WriteFile(configFile, []byte(`
+env:
+  app_name: "my-app"
+  Region: "us-east-1"
+`), 0644)
+	require.NoError(t, err)
+
+	cmd := newTestCommand()
+	_ = cmd.Flags().Set("config", configFile)
+
+	cfg, err := Load(cmd)
+	require.NoError(t, err)
+
+	// Keys should be normalized to uppercase
+	assert.Equal(t, "my-app", cfg.Env["APP_NAME"])
+	assert.Equal(t, "us-east-1", cfg.Env["REGION"])
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/engine/internal/config/config_test.go` around lines 313 - 333, The
test TestLoad_EnvMap currently only asserts behavior with already-uppercase YAML
keys; add an additional subcase that writes env keys in lowercase and mixed-case
(e.g., "app_name", "Region", "Debug") to the temporary mantle.yaml, call
Load(cmd) the same way, and assert that cfg.Env contains the normalized
uppercase keys ("APP_NAME", "REGION", "DEBUG") with the expected values to
verify the strings.ToUpper normalization in Load and the cfg.Env map behavior.
packages/engine/internal/cel/cel.go (1)

211-217: Document or synchronize envCache access to prevent future concurrency issues.

SetConfigEnv writes to e.envCache without synchronization while Eval reads from it. Current usage is safe—SetConfigEnv is called once during engine initialization (serve.go:52, run.go:67) before evaluations begin. However, to prevent data races if the API is used differently in the future, consider adding a sync.RWMutex or a comment documenting the initialization-only requirement.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/engine/internal/cel/cel.go` around lines 211 - 217, SetConfigEnv
writes to e.envCache without synchronization while Eval reads it, risking future
data races; add a sync.RWMutex field (e.g., envMu) to the Evaluator struct and
use envMu.Lock()/Unlock() in SetConfigEnv and envMu.RLock()/RUnlock() in Eval
(and any other readers) when accessing e.envCache, or if you prefer the
lightweight option, add a clear comment on the Evaluator type stating envCache
is initialization-only and must not be mutated after startup; reference
SetConfigEnv, Eval, Evaluator, envCache, and mergeEnvVars when making the
change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/site/src/content/docs/configuration.md`:
- Around line 134-136: Update the fenced code block that contains the log line
"INFO env variable overrides config key=MANTLE_ENV_REGION config_key=env.REGION"
to include a language specifier (e.g., use ```text or ```log) so the snippet
renders with proper syntax highlighting; locate the fenced block in the docs
content around that exact log string and add the language token immediately
after the opening backticks.

---

Nitpick comments:
In `@packages/engine/internal/cel/cel.go`:
- Around line 211-217: SetConfigEnv writes to e.envCache without synchronization
while Eval reads it, risking future data races; add a sync.RWMutex field (e.g.,
envMu) to the Evaluator struct and use envMu.Lock()/Unlock() in SetConfigEnv and
envMu.RLock()/RUnlock() in Eval (and any other readers) when accessing
e.envCache, or if you prefer the lightweight option, add a clear comment on the
Evaluator type stating envCache is initialization-only and must not be mutated
after startup; reference SetConfigEnv, Eval, Evaluator, envCache, and
mergeEnvVars when making the change.

In `@packages/engine/internal/config/config_test.go`:
- Around line 313-333: The test TestLoad_EnvMap currently only asserts behavior
with already-uppercase YAML keys; add an additional subcase that writes env keys
in lowercase and mixed-case (e.g., "app_name", "Region", "Debug") to the
temporary mantle.yaml, call Load(cmd) the same way, and assert that cfg.Env
contains the normalized uppercase keys ("APP_NAME", "REGION", "DEBUG") with the
expected values to verify the strings.ToUpper normalization in Load and the
cfg.Env map behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 777859c4-9802-4919-b3bd-2f9a9ceb0516

📥 Commits

Reviewing files that changed from the base of the PR and between 81daf35 and abc98de.

📒 Files selected for processing (7)
  • packages/engine/internal/cel/cel.go
  • packages/engine/internal/cel/cel_test.go
  • packages/engine/internal/cli/run.go
  • packages/engine/internal/cli/serve.go
  • packages/engine/internal/config/config.go
  • packages/engine/internal/config/config_test.go
  • packages/site/src/content/docs/configuration.md

Comment thread packages/site/src/content/docs/configuration.md Outdated
…block

- Merge main (v0.4.0) into feature branch
- Keep Storage (renamed from Tmp) + add Env field
- Keep concurrency config + add SetConfigEnv wiring
- Keep storage docs + add env.* row and expression variables section
- Add text language specifier to log code block (PR feedback)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
packages/engine/internal/cli/run.go (1)

67-68: Minor ordering inconsistency with serve.go.

In serve.go (lines 51-54), SetConfigEnv is called immediately after engine.New, before setting MaxConcurrentExecutionsPerTeam. Here the order is reversed. While functionally equivalent, aligning the order improves consistency across CLI commands.

🔧 Suggested reordering for consistency
 		eng, err := engine.New(database)
 		if err != nil {
 			return fmt.Errorf("creating engine: %w", err)
 		}
+		eng.CEL.SetConfigEnv(cfg.Env)
 		eng.MaxConcurrentExecutionsPerTeam = cfg.Engine.MaxConcurrentExecutionsPerTeam
-		eng.CEL.SetConfigEnv(cfg.Env)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/engine/internal/cli/run.go` around lines 67 - 68, Reorder the two
assignments so they match serve.go: after creating the engine via engine.New,
call eng.CEL.SetConfigEnv(cfg.Env) immediately, then set
eng.MaxConcurrentExecutionsPerTeam = cfg.Engine.MaxConcurrentExecutionsPerTeam;
adjust the sequence around the eng variable (engine.New, eng.CEL.SetConfigEnv,
then setting MaxConcurrentExecutionsPerTeam) to keep CLI command initialization
consistent.
packages/engine/internal/cel/cel.go (1)

227-233: Consider thread-safety for SetConfigEnv if called concurrently.

envCache is read in Eval (line 88) without synchronization while SetConfigEnv writes to it. Current usage (init-time only in CLI commands) is safe, but if SetConfigEnv is ever called during concurrent execution, this becomes a data race.

If dynamic reconfiguration is planned, consider adding a sync.RWMutex to protect envCache. Otherwise, documenting the "call only during initialization" constraint would be sufficient.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/engine/internal/cel/cel.go` around lines 227 - 233, The SetConfigEnv
method writes to Evaluator.envCache while Eval reads it without synchronization,
creating a potential data race; add a sync.RWMutex field (e.g., envMu) to the
Evaluator struct and use envMu.Lock()/Unlock() in SetConfigEnv when assigning
e.configEnv and e.envCache (and when calling mergeEnvVars), and use
envMu.RLock()/RUnlock() in Eval where it reads e.envCache to protect concurrent
access; if you prefer not to change runtime behavior, instead add a clear
comment on Evaluator.SetConfigEnv and Eval stating that SetConfigEnv must only
be called during initialization.
packages/engine/internal/cel/cel_test.go (1)

300-304: Prefer t.Setenv over manual os.Setenv/defer os.Unsetenv.

Using t.Setenv provides automatic cleanup and is parallel-safe. The manual approach works but is more error-prone.

♻️ Suggested refactor
 func TestEnvVars_PrefixStripping(t *testing.T) {
 	// Directly test the mergeEnvVars function.
-	os.Setenv("MANTLE_ENV_TEST_KEY", "test-value")
-	defer os.Unsetenv("MANTLE_ENV_TEST_KEY")
-
-	os.Setenv("MANTLE_DATABASE_URL", "should-not-appear")
-	defer os.Unsetenv("MANTLE_DATABASE_URL")
+	t.Setenv("MANTLE_ENV_TEST_KEY", "test-value")
+	t.Setenv("MANTLE_DATABASE_URL", "should-not-appear")

 	result := mergeEnvVars(nil)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/engine/internal/cel/cel_test.go` around lines 300 - 304, In the test
in cel_test.go that currently calls os.Setenv("MANTLE_ENV_TEST_KEY", ...) and
os.Setenv("MANTLE_DATABASE_URL", ...) with deferred os.Unsetenv calls, replace
those manual environment manipulations with t.Setenv("MANTLE_ENV_TEST_KEY",
"test-value") and t.Setenv("MANTLE_DATABASE_URL", "should-not-appear")
respectively and remove the corresponding defer os.Unsetenv lines so the
testing.T-managed cleanup (and parallel-safety) is used instead.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/site/src/content/docs/configuration.md`:
- Around line 86-87: The docs and env table disagree on the default for
engine.max_concurrent_executions_per_team: the configuration docs correctly
state default 0 (unlimited) and config.go has no SetDefault so Go zero value is
0, but the Environment Variables table still shows 10; update the Environment
Variables table entry for engine.max_concurrent_executions_per_team to 0 (or
mark as unlimited) to match config.go and the configuration docs, and verify
references to engine.max_concurrent_executions_per_team across docs to ensure
consistency.

---

Nitpick comments:
In `@packages/engine/internal/cel/cel_test.go`:
- Around line 300-304: In the test in cel_test.go that currently calls
os.Setenv("MANTLE_ENV_TEST_KEY", ...) and os.Setenv("MANTLE_DATABASE_URL", ...)
with deferred os.Unsetenv calls, replace those manual environment manipulations
with t.Setenv("MANTLE_ENV_TEST_KEY", "test-value") and
t.Setenv("MANTLE_DATABASE_URL", "should-not-appear") respectively and remove the
corresponding defer os.Unsetenv lines so the testing.T-managed cleanup (and
parallel-safety) is used instead.

In `@packages/engine/internal/cel/cel.go`:
- Around line 227-233: The SetConfigEnv method writes to Evaluator.envCache
while Eval reads it without synchronization, creating a potential data race; add
a sync.RWMutex field (e.g., envMu) to the Evaluator struct and use
envMu.Lock()/Unlock() in SetConfigEnv when assigning e.configEnv and e.envCache
(and when calling mergeEnvVars), and use envMu.RLock()/RUnlock() in Eval where
it reads e.envCache to protect concurrent access; if you prefer not to change
runtime behavior, instead add a clear comment on Evaluator.SetConfigEnv and Eval
stating that SetConfigEnv must only be called during initialization.

In `@packages/engine/internal/cli/run.go`:
- Around line 67-68: Reorder the two assignments so they match serve.go: after
creating the engine via engine.New, call eng.CEL.SetConfigEnv(cfg.Env)
immediately, then set eng.MaxConcurrentExecutionsPerTeam =
cfg.Engine.MaxConcurrentExecutionsPerTeam; adjust the sequence around the eng
variable (engine.New, eng.CEL.SetConfigEnv, then setting
MaxConcurrentExecutionsPerTeam) to keep CLI command initialization consistent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8b6a3ec9-a9a8-47ae-ab52-9882ed48a8bb

📥 Commits

Reviewing files that changed from the base of the PR and between abc98de and d4f6af5.

📒 Files selected for processing (7)
  • packages/engine/internal/cel/cel.go
  • packages/engine/internal/cel/cel_test.go
  • packages/engine/internal/cli/run.go
  • packages/engine/internal/cli/serve.go
  • packages/engine/internal/config/config.go
  • packages/engine/internal/config/config_test.go
  • packages/site/src/content/docs/configuration.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/engine/internal/cli/serve.go
  • packages/engine/internal/config/config_test.go

Comment thread packages/site/src/content/docs/configuration.md

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/site/src/content/docs/configuration.md`:
- Around line 104-162: Update the docs to explicitly state that env keys are
case-sensitive and must be uppercase when using MANTLE_ENV_* overrides: explain
that config keys are normalized to uppercase (e.g., env keys like APP_NAME,
REGION) and that OS vars after stripping the MANTLE_ENV_ prefix must also be
provided in uppercase (MANTLE_ENV_REGION) or they will not match the normalized
keys in the env map (causing duplicate keys like REGION and region), and
reference the env:<KEY> usage (env.APP_NAME) and the MANTLE_ENV_* prefix so
readers know exactly which names to use.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9abc674a-3191-44ff-9805-a3eb0a1a880a

📥 Commits

Reviewing files that changed from the base of the PR and between d4f6af5 and cdaab76.

📒 Files selected for processing (1)
  • packages/site/src/content/docs/configuration.md

Comment thread packages/site/src/content/docs/configuration.md
@michaelmcnees michaelmcnees merged commit e73029e into main Mar 27, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] Allow env: map in mantle.yaml for workflow expression variables

1 participant